﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Mono.Cecil;

namespace Cubicle.Core.Shared
{
    public static class TypeCache
    {
        // Nlog
        private static NLog.Logger _logger = NLog.LogManager.GetCurrentClassLogger();

        // .Net assembly tokens
        private static string[] _dotNetKeyTokenSet = { "b77a5c561934e089", "b03f5f7f11d50a3a", "31bf3856ad364e35" } ;

        // Type.FullName -> Assembly
        private static readonly Dictionary<string, Assembly> Types = new Dictionary<string, Assembly>();
        
        // Assembly.FullName -> Assembly
        private static readonly Dictionary<string, Assembly> FullNames = new Dictionary<string, Assembly>();

        // Assembly.FullName -> Cecil assembly definition
        private static readonly Dictionary<string, AssemblyDefinition> Definitions =
            new Dictionary<string, AssemblyDefinition>();

        // Assembly.FullName -> List of full names of missing dependencies
        private static readonly Dictionary<string, List<string>> Missing = new Dictionary<string, List<string>>();

        // Contains aliases to types
        private static readonly Dictionary<string, Type> AliasRegister = new Dictionary<string, Type>();

        // Register aliases
        static TypeCache()
        {
            // Value types
            // Some extra aliases have been defined to simplify construction
            RegisterTypeAlias("bool", typeof(bool));
            RegisterTypeAlias("byte", typeof(byte));
            RegisterTypeAlias("uint8", typeof(byte));          // Notice how Byte  = UInt8
            RegisterTypeAlias("sbyte", typeof(sbyte));         // Notice how SByte = Int8
            RegisterTypeAlias("int8", typeof(sbyte));
            RegisterTypeAlias("char", typeof(char));
            RegisterTypeAlias("decimal", typeof(decimal));
            RegisterTypeAlias("double", typeof(double));
            RegisterTypeAlias("float", typeof(float));
            RegisterTypeAlias("single", typeof(float));
            RegisterTypeAlias("int", typeof(int));
            RegisterTypeAlias("int32", typeof(int));
            RegisterTypeAlias("uint", typeof(uint));
            RegisterTypeAlias("uint32", typeof(uint));
            RegisterTypeAlias("long", typeof(long));
            RegisterTypeAlias("int64", typeof(long));
            RegisterTypeAlias("ulong", typeof(ulong));
            RegisterTypeAlias("uint64", typeof(ulong));
            RegisterTypeAlias("short", typeof(short));
            RegisterTypeAlias("int16", typeof(short));
            RegisterTypeAlias("ushort", typeof(ushort));
            RegisterTypeAlias("uint16", typeof(ushort));
            RegisterTypeAlias("string", typeof(string));
            RegisterTypeAlias("object", typeof(object));
            RegisterTypeAlias("system.boolean", typeof(bool));
            RegisterTypeAlias("system.byte", typeof(byte));
            RegisterTypeAlias("system.sbyte", typeof(sbyte));
            RegisterTypeAlias("system.char", typeof(char));
            RegisterTypeAlias("system.decimal", typeof(decimal));
            RegisterTypeAlias("system.double", typeof(double));
            RegisterTypeAlias("system.single", typeof(float));
            RegisterTypeAlias("system.int32", typeof(int));
            RegisterTypeAlias("system.uint32", typeof(uint));
            RegisterTypeAlias("system.int64", typeof(long));
            RegisterTypeAlias("system.uint64", typeof(ulong));
            RegisterTypeAlias("system.int16", typeof(short));
            RegisterTypeAlias("system.uint16", typeof(ushort));
            RegisterTypeAlias("system.string", typeof(string));
            RegisterTypeAlias("system.object", typeof(object));

            // Single Array types
            RegisterTypeAlias("bool[]", typeof(bool[]));
            RegisterTypeAlias("byte[]", typeof(byte[]));
            RegisterTypeAlias("uint8[]", typeof(byte[]));          // Notice how Byte  = UInt8
            RegisterTypeAlias("sbyte[]", typeof(sbyte[]));         // Notice how SByte = Int8
            RegisterTypeAlias("int8[]", typeof(sbyte[]));
            RegisterTypeAlias("char[]", typeof(char[]));
            RegisterTypeAlias("decimal[]", typeof(decimal[]));
            RegisterTypeAlias("double[]", typeof(double[]));
            RegisterTypeAlias("float[]", typeof(float[]));
            RegisterTypeAlias("single[]", typeof(float[]));
            RegisterTypeAlias("int[]", typeof(int[]));
            RegisterTypeAlias("int32[]", typeof(int[]));
            RegisterTypeAlias("uint[]", typeof(uint[]));
            RegisterTypeAlias("uint32[]", typeof(uint[]));
            RegisterTypeAlias("long[]", typeof(long[]));
            RegisterTypeAlias("int64[]", typeof(long[]));
            RegisterTypeAlias("ulong[]", typeof(ulong[]));
            RegisterTypeAlias("uint64[]", typeof(ulong[]));
            RegisterTypeAlias("short[]", typeof(short[]));
            RegisterTypeAlias("int16[]", typeof(short[]));
            RegisterTypeAlias("ushort[]", typeof(ushort[]));
            RegisterTypeAlias("uint16[]", typeof(ushort[]));
            RegisterTypeAlias("string[]", typeof(string[]));
            RegisterTypeAlias("object[]", typeof(object[]));
            RegisterTypeAlias("system.boolean[]", typeof(bool[]));
            RegisterTypeAlias("system.byte[]", typeof(byte[]));
            RegisterTypeAlias("system.sbyte[]", typeof(sbyte[]));
            RegisterTypeAlias("system.char[]", typeof(char[]));
            RegisterTypeAlias("system.decimal[]", typeof(decimal[]));
            RegisterTypeAlias("system.double[]", typeof(double[]));
            RegisterTypeAlias("system.single[]", typeof(float[]));
            RegisterTypeAlias("system.int32[]", typeof(int[]));
            RegisterTypeAlias("system.uint32[]", typeof(uint[]));
            RegisterTypeAlias("system.int64[]", typeof(long[]));
            RegisterTypeAlias("system.uint64[]", typeof(ulong[]));
            RegisterTypeAlias("system.int16[]", typeof(short[]));
            RegisterTypeAlias("system.uint16[]", typeof(ushort[]));
            RegisterTypeAlias("system.string[]", typeof(string[]));
            RegisterTypeAlias("system.object[]", typeof(object[]));

            // Jagged Array types
            RegisterTypeAlias("bool[][]", typeof(bool[][]));
            RegisterTypeAlias("byte[][]", typeof(byte[][]));
            RegisterTypeAlias("uint8[][]", typeof(byte[][]));          // Notice how Byte  = UInt8
            RegisterTypeAlias("sbyte[][]", typeof(sbyte[][]));         // Notice how SByte = Int8
            RegisterTypeAlias("int8[][]", typeof(sbyte[][]));
            RegisterTypeAlias("char[][]", typeof(char[][]));
            RegisterTypeAlias("decimal[][]", typeof(decimal[][]));
            RegisterTypeAlias("double[][]", typeof(double[][]));
            RegisterTypeAlias("float[][]", typeof(float[][]));
            RegisterTypeAlias("single[][]", typeof(float[][]));
            RegisterTypeAlias("int[][]", typeof(int[][]));
            RegisterTypeAlias("int32[][]", typeof(int[][]));
            RegisterTypeAlias("uint[][]", typeof(uint[][]));
            RegisterTypeAlias("uint32[][]", typeof(uint[][]));
            RegisterTypeAlias("long[][]", typeof(long[][]));
            RegisterTypeAlias("int64[][]", typeof(long[][]));
            RegisterTypeAlias("ulong[][]", typeof(ulong[][]));
            RegisterTypeAlias("uint64[][]", typeof(ulong[][]));
            RegisterTypeAlias("short[][]", typeof(short[][]));
            RegisterTypeAlias("int16[][]", typeof(short[][]));
            RegisterTypeAlias("ushort[][]", typeof(ushort[][]));
            RegisterTypeAlias("uint16[][]", typeof(ushort[][]));
            RegisterTypeAlias("string[][]", typeof(string[][]));
            RegisterTypeAlias("object[]", typeof(object[][]));
            RegisterTypeAlias("system.boolean[][]", typeof(bool[][]));
            RegisterTypeAlias("system.byte[][]", typeof(byte[][]));
            RegisterTypeAlias("system.sbyte[][]", typeof(sbyte[][]));
            RegisterTypeAlias("system.char[][]", typeof(char[][]));
            RegisterTypeAlias("system.decimal[][]", typeof(decimal[][]));
            RegisterTypeAlias("system.double[][]", typeof(double[][]));
            RegisterTypeAlias("system.single[][]", typeof(float[][]));
            RegisterTypeAlias("system.int32[][]", typeof(int[][]));
            RegisterTypeAlias("system.uint32[][]", typeof(uint[][]));
            RegisterTypeAlias("system.int64[][]", typeof(long[][]));
            RegisterTypeAlias("system.uint64[][]", typeof(ulong[][]));
            RegisterTypeAlias("system.int16[][]", typeof(short[][]));
            RegisterTypeAlias("system.uint16[][]", typeof(ushort[][]));
            RegisterTypeAlias("system.string[][]", typeof(string[][]));
            RegisterTypeAlias("system.object[][]", typeof(object[][]));

            // Double Array types
            RegisterTypeAlias("bool[,]", typeof(bool[,]));
            RegisterTypeAlias("byte[,]", typeof(byte[,]));
            RegisterTypeAlias("uint8[,]", typeof(byte[,]));          // Notice how Byte  = UInt8
            RegisterTypeAlias("sbyte[,]", typeof(sbyte[,]));         // Notice how SByte = Int8
            RegisterTypeAlias("int8[,]", typeof(sbyte[,]));
            RegisterTypeAlias("char[,]", typeof(char[,]));
            RegisterTypeAlias("decimal[,]", typeof(decimal[,]));
            RegisterTypeAlias("double[,]", typeof(double[,]));
            RegisterTypeAlias("float[,]", typeof(float[,]));
            RegisterTypeAlias("single[,]", typeof(float[,]));
            RegisterTypeAlias("int[,]", typeof(int[,]));
            RegisterTypeAlias("int32[,]", typeof(int[,]));
            RegisterTypeAlias("uint[,]", typeof(uint[,]));
            RegisterTypeAlias("uint32[,]", typeof(uint[,]));
            RegisterTypeAlias("long[,]", typeof(long[,]));
            RegisterTypeAlias("int64[,]", typeof(long[,]));
            RegisterTypeAlias("ulong[,]", typeof(ulong[,]));
            RegisterTypeAlias("uint64[,]", typeof(ulong[,]));
            RegisterTypeAlias("short[,]", typeof(short[,]));
            RegisterTypeAlias("int16[,]", typeof(short[,]));
            RegisterTypeAlias("ushort[,]", typeof(ushort[,]));
            RegisterTypeAlias("uint16[,]", typeof(ushort[,]));
            RegisterTypeAlias("string[,]", typeof(string[,]));
            RegisterTypeAlias("object[,]", typeof(object[,]));
            RegisterTypeAlias("system.boolean[,]", typeof(bool[,]));
            RegisterTypeAlias("system.byte[,]", typeof(byte[,]));
            RegisterTypeAlias("system.sbyte[,]", typeof(sbyte[,]));
            RegisterTypeAlias("system.char[,]", typeof(char[,]));
            RegisterTypeAlias("system.decimal[,]", typeof(decimal[,]));
            RegisterTypeAlias("system.double[,]", typeof(double[,]));
            RegisterTypeAlias("system.single[,]", typeof(float[,]));
            RegisterTypeAlias("system.int32[,]", typeof(int[,]));
            RegisterTypeAlias("system.uint32[,]", typeof(uint[,]));
            RegisterTypeAlias("system.int64[,]", typeof(long[,]));
            RegisterTypeAlias("system.uint64[,]", typeof(ulong[,]));
            RegisterTypeAlias("system.int16[,]", typeof(short[,]));
            RegisterTypeAlias("system.uint16[,]", typeof(ushort[,]));
            RegisterTypeAlias("system.string[,]", typeof(string[,]));
            RegisterTypeAlias("system.object[,]", typeof(object[,]));

            // List types
            RegisterTypeAlias("list", typeof(List<>));
            RegisterTypeAlias("dict", typeof(Dictionary<,>));
            RegisterTypeAlias("dictionary", typeof(Dictionary<,>));
        }

        public static bool IsAlreadyLoaded(byte[] assembly)
        {
            var manifest = Helpers.ReadCecilDefinition(assembly);
            return manifest.FullName != null
                && AppDomain.CurrentDomain
                .GetAssemblies()
                .Select(x => x.FullName)
                .Contains(manifest.FullName);
        }

        public static void RegisterTypeAlias(string alias, Type type)
        {
            AliasRegister[alias.ToLower()] = type;
        }

        public static void RegisterTypeAlias(string alias, string typeFullName)
        {
            var type = Type.GetType(typeFullName);
            if (type == null) return;
            RegisterTypeAlias(alias, type);
        }

        public static Assembly GetAssemblyFromName(string fullName)
        {
            return AppDomain
                .CurrentDomain
                .GetAssemblies()
                .Where(x => x.FullName == fullName)
                .FirstOrDefault();
        }

        /// <summary>
        /// Resolve a type alias or fullName to the underlying type
        /// </summary>
        /// <param name="alias">An alias or full name</param>
        /// <returns>The underlying type</returns>
        public static Type GetType(string alias)
        {
            var aliasLower = alias.ToLower();
            return AliasRegister.ContainsKey(aliasLower)
                ? AliasRegister[aliasLower] 
                : Type.GetType(alias);
        }

        public static IEnumerable<KeyValuePair<string, string>> ListTypeAliases()
        {
            return AliasRegister.ToDictionary(x => x.Key, x => x.Value.FullName);
        }

        public static IEnumerable<Type> ListCachedTypes()
        {
            return Types
                .Select(x => x.Value.GetType(x.Key))
                .Where(x => x != null)
                .ToList();
        }

        public static string LoadAssembly(byte[] assembly, byte[] symbols)
        {
            try
            {
                // Now load more assemblies
                if (IsAlreadyLoaded(assembly))
                    return Helpers.ReadCecilDefinition(assembly).FullName;

                CacheCecilDefinition(assembly);
                var asm = symbols == null
                    ? Assembly.Load(assembly)
                    : Assembly.Load(assembly, symbols);
                return asm.FullName;
            }
            catch (Exception e)
            {
                _logger.ErrorException(e, "Error loading assembly from bytes");
                return null;
            }
        }

        public static string LoadAssembly(string fileName)
        {
            var asm = Helpers.ReadBytesFromFile(fileName);
            return LoadAssembly(asm, null);
        }

        public static Assembly AutoTypeResolve(object sender, ResolveEventArgs args)
        {
            Assembly result = null;
            Types.TryGetValue(args.Name, out result);
            return result;
        }

        public static Assembly AutoAssemblyResolve(object sender, ResolveEventArgs args)
        {
            Assembly result = null;
            FullNames.TryGetValue(args.Name, out result);
            return result;
        }

        public static void AutoTypeCache(object sender, AssemblyLoadEventArgs args)
        {
            // Don't cache assemblies loaded by reflection only
            if (args.LoadedAssembly.ReflectionOnly) return;

            // Perform caching
            var asm = args.LoadedAssembly;
            CacheAssembly(asm);
        }

        public static Type GetType(string typeNamespace, string typeName)
        {
            Assembly result = null;
            var fullName = typeNamespace + "." + typeName;
            Types.TryGetValue(fullName, out result);
            return result == null 
                ? null 
                : result.GetType(fullName, false);
        }

        public static void CacheAssembly(Assembly asm)
        {
            // Don't cache if it is dynamic or null
            if (asm == null || asm.IsDynamic || IsDotNetAssembly(asm)) return;

            // Don't cache if it's already been cached and has no missing dependencies.
            if (FullNames.ContainsKey(asm.FullName) && !Missing.ContainsKey(asm.FullName)) return;

            // Cache FullName
            FullNames[asm.FullName] = asm;

            // Remove entry from missing, this will be regenerated below
            Missing.Remove(asm.FullName);

            // Check if the cecil definition is available
            AssemblyDefinition def;
            Definitions.TryGetValue(asm.FullName, out def);

            // If not, try to get it
            // This will fail if the assembly only exists in-memory and not on file
            if (def == null)
            {
                def = asm.GetCecilDefinition();
                if (def != null)
                    Definitions[asm.FullName] = def;
            }

            // Strategy 1: Cache types using Cecil definition
            if (def != null)
            {
                // Check if that this Assembly is not a wrapper without Modules
                try
                {
                    var modules = def.Modules;
                } catch (Exception e)
                {
                    FullNames.Remove(asm.FullName);
                    Definitions.Remove(asm.FullName);
                    _logger.ErrorException(e, "Type cache error. There was a problem reflecting assembly modules. FullName = {0}",
                              asm.FullName);
                    return;
                }
                var types = def.Modules.SelectMany(m => m.Types);

                // Cache types using Cecil (reflection throws an exception if all dependencies
                // are not loaded)
                foreach (var t in types)
                    Types[t.FullName ?? t.Name] = asm;

                _logger.Info("Assembly types cached using Cecil. FullName = {0}", asm.FullName);
            } else
            {
                // Strategy 2: Cache types using reflection (this will throw a
                // ReflectionTypeLoadException if dependencies are not loaded)
                // Types with unloaded dependencies will not be cached
                List<string> missingDep;
                var types = asm.SafeGetTypes(out missingDep, true);
                if (missingDep != null && missingDep.Count > 0)
                    Missing[asm.FullName] = missingDep;

                // Cache types
                foreach (var t in types)
                    // We have to cache full names here, otherwise resolution won't work
                    Types[t.FullName ?? t.Name] = asm;
            }

            // Recache all assemblies for which this assembly was a missing dependency
            var recache = from item1 in Missing
                          join item2 in FullNames on item1.Key equals item2.Key
                          where item1.Value.Contains(asm.FullName)
                          select item2.Value;

            foreach (var item in recache)
                CacheAssembly(item);
        }

        public static void CacheAll()
        {
            var asms = AppDomain.CurrentDomain.GetAssemblies();
            foreach (var asm in asms)
                CacheAssembly(asm);
        }

        public static bool IsDotNetAssembly(byte[] asm)
        {
            return IsDotNetAssembly(Helpers.ReadCecilDefinition(asm).Name.FullName);
        }

        public static bool IsDotNetAssembly(Assembly asm)
        {
            return IsDotNetAssembly(asm.FullName);
        }

        public static bool IsDotNetAssembly(AssemblyDefinition asm)
        {
            return IsDotNetAssembly(asm.FullName);
        }

        public static bool IsDotNetAssembly(string fullName)
        {
            try
            {
                var keyToken = fullName.Split('=')[3];
                return _dotNetKeyTokenSet.Contains(keyToken);
            }
            catch (Exception)
            {
                return false;
            }
        }

        public static void CacheCecilDefinition(byte[] assembly)
        {
            // Use Cecil to get desc
            var desc = Helpers.ReadCecilDefinition(assembly);

            // Cache the Cecil assembly description
            if (!IsDotNetAssembly(desc))
                Definitions[desc.FullName] = desc;
        }

        public static AssemblyDefinition GetCecilDefinition(this Assembly asm)
        {
            try
            {
                // Strategy 1: check the cache
                if (Definitions.ContainsKey(asm.FullName))
                    return Definitions[asm.FullName];

                // Strategy 2: read from disk
                var bytes = Helpers.ReadBytesFromFile(asm.Location);
                if (bytes != null)
                    return Helpers.ReadCecilDefinition(bytes);
            } catch (Exception e)
            {
                _logger.ErrorException(e, "Error trying to load Cecil definition. FullName = {0}", asm.FullName);
            }

            return null;
        }

        public static IEnumerable<TypeDefinition> GetCecilTypes(this Assembly asm)
        {
            var def = asm.GetCecilDefinition();
            return def == null 
                ? null 
                : def.MainModule.Types;
        }

    }
}